﻿using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Extensions;

namespace FluentTagsGenerator
{
    class TagElements: IEnumerable<TagElement>
    {
        /// <summary>
        /// A list of all tags, with a boolean value indicationg
        /// whether this is a element that has a text display.
        /// </summary>
        static readonly Dictionary<string, bool> TAGS =
            new Dictionary<string, bool>()
                { 
                    { "a", true }, 
                    { "b", true }, 
                    { "blockquote", true },
                    { "br", false },
                    { "div", true },
                    { "em", true },
                    { "h1", true },
                    { "h2", true },
                    { "h3", true },
                    { "h4", true },
                    { "h5", true },
                    { "h6", true },
                    { "hr", false },
                    { "i", true },
                    { "img", false },
                    { "li", true },
                    { "ol", false },
                    { "p", true },
                    { "pre", true },
                    { "span", true },
                    { "strong", true },
                    { "sub", true },
                    { "sup", true },
                    { "table", false },
                    { "th", true },
                    { "tr", true },
                    { "td", true },
                    { "ul", false }
                };


        const string REF = "ref"; 
        private List<TagElement> _elements = null;
        private XNamespace _nameSpace = null;
        private XDocument _xmlDoc = null;

        public TagElements(string file)
        {
            _xmlDoc = XDocument.Load(file, LoadOptions.SetBaseUri);
            _nameSpace = XNamespace.Get("http://www.w3.org/2001/XMLSchema");

            // Elements look like this:
            /*
             <xs:element name="span">
                <xs:annotation>
                  <xs:documentation>
                  generic language/style container
                  </xs:documentation>
                </xs:annotation>
                <xs:complexType mixed="true">
                  <xs:complexContent>
                    <xs:extension base="Inline">
                      <xs:attributeGroup ref="attrs"/>
                    </xs:extension>
                  </xs:complexContent>
                </xs:complexType>
              </xs:element>
             */
            var xmlElements = GetXmlTagElements("element");

            _elements = new List<TagElement>();

            foreach (XElement x in xmlElements)
            {
                var element = CreateElement(x);

                foreach (XElement xe in x.DescendantNodes().OfType<XElement>())
                {
                    if (xe.Name == XName("extension"))
                    {
                        GetGroupChildren(element, xe);
                    }
                    else if (xe.Name == XName("attribute"))
                    {
                        GetAttribute(element, xe);
                    }
                    else if (xe.Name == XName("element"))
                    {
                        GetElement(element, xe);
                    }
                    else if (xe.Name == XName("documentation"))
                    {
                        element.Comment = xe.Value.Trim();
                    }
                }

                element.Attributes.Sort();

                _elements.Add(element);
            }

            _elements.Sort();
        }

        private static void GetElement(TagElement element, XElement xe)
        {
            string childElementName = xe.AttributeValue(REF);
            if (TAGS.ContainsKey(childElementName))
            {
                element.AddChildElement(childElementName);
            }
        }

        private static void GetAttribute(TagElement element, XElement xe)
        {
            string attrValue = null;
            if (xe.TryGetAttributeValue("name", out attrValue))
            {
                var attr = new Attribute(xe.AttributeValue("name"));

                attr.XmlType = xe.AttributeValueorDefault("type", "");

                attr.Default = xe.AttributeValueorDefault("default", "");

                element.Attributes.Add(attr);
            }
        }

        private void GetGroupChildren(TagElement element, XElement xe)
        {
            // we have a line like this:
            // <xs:extension base="Inline">
            // so we need to find all the elements of the Inline group
            string groupName = xe.AttributeValue("base");

            // a complexType has 0 to N groups and elements
            // a group has 0 to N groups and elements
            // so we must recursively search for all elements/groups in this complexType
            var childElements = FindComplexTypeElements(groupName);

            element.AddChildElements(childElements);
        }

        private XName XName(string name)
        {
            return _nameSpace + name;
        }

        private static TagElement CreateElement(XElement x)
        {
            var elementName = x.AttributeValue("name");
            var isText = TAGS[elementName];
            var element = new TagElement(elementName, isText);
            return element;
        }

        private IEnumerable<XElement> GetXmlTagElements(string elementType)
        {
            var xmlElements = from e in _xmlDoc.Descendants(XName(elementType))
                              where e.Attributes("name").Any() == true
                              && (TAGS.ContainsKey(e.AttributeValue("name")))
                              select e;
            return xmlElements;
        }

        private List<string> FindComplexTypeElements(string name)
        {
            return FindNestedElements(name, "complexType");
        }

        private List<string> FindGroupElements(string name)
        {
            return FindNestedElements(name, "group");
        }

        private List<string> FindNestedElements(string name, string elementType)
        {
            var xElements = GetDescendants(name, elementType);

            var elementNames = new List<string>();

            foreach (XElement xElement in xElements)
            {
                var descendants = xElement.DescendantNodes().OfType<XElement>();

                foreach (XElement descElement in descendants)
                {
                    if (descElement.Name == XName("group"))
                    {
                        string groupName = descElement.AttributeValue(REF);
                        elementNames.AddRange(FindGroupElements(groupName));
                    }
                    else if (descElement.Name == XName("element"))
                    {
                        string elementName = descElement.AttributeValue(REF);
                        if (TAGS.ContainsKey(elementName))
                        {
                            elementNames.Add(elementName);
                        }
                    }
                }
            }

            return elementNames;
        }

        private IEnumerable<XElement> GetDescendants(string name, string elementType)
        {
            var xElements = from e in _xmlDoc.Descendants(XName(elementType))
                            where (e.Attributes("name").Any() == true)
                            && (name == e.AttributeValue("name"))
                            select e;
            return xElements;
        }

        public TagElement Find(System.Predicate<TagElement> match)
        {
            return _elements.Find(match);
        }

        public IEnumerator<TagElement> GetEnumerator()
        {
            return _elements.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _elements.GetEnumerator();
        }
    }
}
